package com.tongjie.hotelbooking.im.websocket;

import com.alibaba.fastjson.JSON;
import com.tongjie.hotelbooking.dto.ImMessageDto;
import com.tongjie.hotelbooking.im.conversation.entiry.Conversation;
import com.tongjie.hotelbooking.im.conversation.entiry.MemberConversation;
import com.tongjie.hotelbooking.im.conversation.service.MemberConversationService;
import com.tongjie.hotelbooking.im.message.entiry.ImMessage;
import com.tongjie.hotelbooking.im.message.entiry.MessageType;
import com.tongjie.hotelbooking.im.message.service.ImMessageService;
import com.tongjie.hotelbooking.user.entity.User;
import com.tongjie.hotelbooking.user.service.UserService;
import com.tongjie.hotelbooking.utils.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@ServerEndpoint("/websocket/endpoint/{userId}")
@Component
@Slf4j
public class WebSocketEndpoint {

    private static final String PING = "ping";

    private static final String PONG = "pong";

    private static Map<String, SessionWrapper> clients = new ConcurrentHashMap<>();

    private static Map<Long, String> userIdToSessionMapping = new ConcurrentHashMap<>();

//    private static MemberConversationService memberConversationService;

    private static final String sessionLock = "sessionLock";

    @OnOpen
    public void onOpen(@PathParam("userId") Long userId, Session session) {
        synchronized (sessionLock) {
            String existsSessionId = userIdToSessionMapping.remove(userId);
            if (existsSessionId != null) {
                SessionWrapper oldSessionWrapper = clients.remove(existsSessionId);
                if (oldSessionWrapper != null) {
                    try {
                        oldSessionWrapper.getSession().close();
                    } catch (IOException e) {
                        log.error("session close failed", e);
                    }
                }
            }
            log.info("connection established, userId={}, sessionId={}", userId, session.getId());
            userIdToSessionMapping.put(userId, session.getId());
            clients.put(session.getId(), SessionWrapper.builder().session(session).userId(userId).build());
        }
    }

    @OnClose
    public void onClose(Session session) {
        synchronized (sessionLock) {
            SessionWrapper sessionWrapper = clients.remove(session.getId());
            if (sessionWrapper != null) {
                try {
                    log.info("connection closed, sessionId={}", session.getId());
                    sessionWrapper.getSession().close();
                } catch (IOException e) {
                    log.error("websocket close failed", e);
                }
                userIdToSessionMapping.remove(sessionWrapper.getUserId());
            }
        }
    }

    @OnError
    public void onError(Throwable throwable) {
        log.error("error", throwable);
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("{}", message);

        if (PING.equals(message)) {
            SessionWrapper sessionWrapper = clients.get(session.getId());
            sessionWrapper.setHeartbeatTime(System.currentTimeMillis());
            session.getAsyncRemote().sendText(PONG);
            return;
        }

        ImMessageDto webSocketMessageDto = JSON.parseObject(message, ImMessageDto.class);

        Date sendDate = new Date();
        webSocketMessageDto.setSendDate(DateFormatUtils.format(sendDate, "yyyy-MM-dd HH:mm:ss"));
        this.saveMessage(webSocketMessageDto, sendDate);

        User sendUser = SpringUtil.getBean(UserService.class).getUserById(webSocketMessageDto.getSenderId());
        webSocketMessageDto.setAvatar(sendUser.getAvatar());

        List<MemberConversation> memberConversations = SpringUtil.getBean(MemberConversationService.class).findMemberConversationByConversationId(webSocketMessageDto.getConversationId());
        this.broadcastMessage(memberConversations.stream().map(memberConversation -> memberConversation.getMemberUser().getId()).collect(Collectors.toList()), webSocketMessageDto);
    }

    private void saveMessage(ImMessageDto webSocketMessageDto, Date sendDate) {
        //TODO recommend to make this method async
        ImMessage imMessage = ImMessage.builder()
                .conversation(Conversation.builder().id(webSocketMessageDto.getConversationId()).build())
                .senderUser(User.builder().id(webSocketMessageDto.getSenderId()).build())
                .sendTime(sendDate)
                .messageType(webSocketMessageDto.getMessageType()).build();

        if (MessageType.TEXT.equals(webSocketMessageDto.getMessageType())) {
            imMessage.setTextMessage(webSocketMessageDto.getMessageBody());
        } else if (MessageType.IMAGE.equals(webSocketMessageDto.getMessageType())) {
            imMessage.setImageUrl(webSocketMessageDto.getMessageBody());
        } else if (MessageType.TEMPLATE.equals(webSocketMessageDto.getMessageType())) {
            imMessage.setHtml(webSocketMessageDto.getMessageBody());
        }

        SpringUtil.getBean(ImMessageService.class).save(imMessage);
    }

    private void broadcastMessage(List<Long> targetUserIds, ImMessageDto webSocketMessageDto) {
        for (Long targetUserId : targetUserIds) {
            this.sendMessage(targetUserId, webSocketMessageDto);
        }
    }

    private void sendMessage(Long userId, ImMessageDto webSocketMessageDto) {
        String sessionKey = userIdToSessionMapping.get(userId);
        if (StringUtils.isBlank(sessionKey)) {
            log.error("sessionKey not found, userId={}", userId);
            return;
        }

        SessionWrapper sessionWrapper = clients.get(sessionKey);
        if (sessionWrapper == null) {
            log.error("websocket session not found, sessionKey={}, userId={}", sessionKey, userId);
            return;
        }

        String jsonStr = JSON.toJSONString(webSocketMessageDto);

        log.info("push message to websocket client, userId={}, sessionKey={}, message={}", userId, sessionKey, jsonStr);
        sessionWrapper.getSession().getAsyncRemote().sendText(jsonStr);
    }

    public static void closeConnection(Long userId) {
        String sessionKey = userIdToSessionMapping.remove(userId);
        if (sessionKey != null) {
            SessionWrapper sessionWrapper = clients.remove(sessionKey);
            if (sessionWrapper != null) {
                try {
                    sessionWrapper.getSession().close();
                } catch (IOException e) {
                    log.error("close websocket connection failed, userId={}, sessionKey={}", userId, sessionKey);
                }
            }
        }
    }

}
